package com.fourtwenty.hemptress.plugin;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fourtwenty.hemptress.BotResponse;
import com.fourtwenty.hemptress.PluginEventBot;
import com.fourtwenty.hemptress.plugin.annotate.events.OnAction;
import com.fourtwenty.hemptress.plugin.annotate.events.OnChannelMessage;
import com.fourtwenty.hemptress.plugin.annotate.events.OnCommand;
import com.fourtwenty.hemptress.plugin.annotate.events.OnConnect;
import com.fourtwenty.hemptress.plugin.annotate.events.OnDeOp;
import com.fourtwenty.hemptress.plugin.annotate.events.OnDeVoice;
import com.fourtwenty.hemptress.plugin.annotate.events.OnDisconnect;
import com.fourtwenty.hemptress.plugin.annotate.events.OnInvite;
import com.fourtwenty.hemptress.plugin.annotate.events.OnJoin;
import com.fourtwenty.hemptress.plugin.annotate.events.OnNickChange;
import com.fourtwenty.hemptress.plugin.annotate.events.OnOp;
import com.fourtwenty.hemptress.plugin.annotate.events.OnPart;
import com.fourtwenty.hemptress.plugin.annotate.events.OnPrivMsg;
import com.fourtwenty.hemptress.plugin.annotate.events.OnQuit;
import com.fourtwenty.hemptress.plugin.annotate.events.OnTopic;
import com.fourtwenty.hemptress.plugin.annotate.events.OnVoice;
import com.fourtwenty.hemptress.plugin.annotate.parameters.Action;
import com.fourtwenty.hemptress.plugin.annotate.parameters.Channel;
import com.fourtwenty.hemptress.plugin.annotate.parameters.Command;
import com.fourtwenty.hemptress.plugin.annotate.parameters.Date;
import com.fourtwenty.hemptress.plugin.annotate.parameters.Message;
import com.fourtwenty.hemptress.plugin.annotate.parameters.NewNick;
import com.fourtwenty.hemptress.plugin.annotate.parameters.OldNick;
import com.fourtwenty.hemptress.plugin.annotate.parameters.Params;
import com.fourtwenty.hemptress.plugin.annotate.parameters.Reply;
import com.fourtwenty.hemptress.plugin.annotate.parameters.SenderHostname;
import com.fourtwenty.hemptress.plugin.annotate.parameters.SenderLogin;
import com.fourtwenty.hemptress.plugin.annotate.parameters.SenderNick;
import com.fourtwenty.hemptress.plugin.annotate.parameters.Target;

public class PluginEventSource {
	private static final Logger LOG = LoggerFactory.getLogger(PluginEventSource.class);
	private ExecutorService executor = Executors.newSingleThreadExecutor();
	
	public PluginEventSource() {
		//to-do: move this to config file
		registerAnnotation(Action.class, "action");
		registerAnnotation(Message.class, "message");
		registerAnnotation(Channel.class, "channel");
		registerAnnotation(Date.class, "date");
		registerAnnotation(NewNick.class, "newnick");
		registerAnnotation(OldNick.class, "oldnick");
		registerAnnotation(SenderHostname.class, "senderhostname");
		registerAnnotation(SenderLogin.class, "senderlogin");
		registerAnnotation(SenderNick.class, "sendernick");
		registerAnnotation(Target.class, "target");
		registerAnnotation(Command.class, "command");
		registerAnnotation(Params.class, "params");
		registerAnnotation(Reply.class, "reply");
		
		registerAnnotation(OnAction.class, PluginEvent.ACTION);
		registerAnnotation(OnChannelMessage.class, PluginEvent.CHANNEL_MESSAGE);
		registerAnnotation(OnPrivMsg.class, PluginEvent.PRIVMSG);
		registerAnnotation(OnJoin.class, PluginEvent.JOIN);
		registerAnnotation(OnNickChange.class, PluginEvent.NICK_CHANGE);
		registerAnnotation(OnOp.class, PluginEvent.OP);
		registerAnnotation(OnPart.class, PluginEvent.PART);
		registerAnnotation(OnInvite.class, PluginEvent.INVITE);
		registerAnnotation(OnDisconnect.class, PluginEvent.DISCONNECT);
		registerAnnotation(OnConnect.class, PluginEvent.CONNECT);
		registerAnnotation(OnDeOp.class, PluginEvent.DEOP);
		registerAnnotation(OnDeVoice.class, PluginEvent.DEVOICE);
		registerAnnotation(OnVoice.class, PluginEvent.VOICE);
		registerAnnotation(OnTopic.class, PluginEvent.TOPIC);
		registerAnnotation(OnQuit.class, PluginEvent.QUIT);
		registerAnnotation(OnCommand.class, PluginEvent.COMMAND);
	}
	
	private EnumMap<PluginEvent, List<MethodContainer>> eventMap = new EnumMap<>(PluginEvent.class);
	private Map<String, List<MethodContainer>> commands = new HashMap<>();
	
	private Set<String> enabledPlugins = new HashSet<>();
	private Set<String> disabledPlugins = new HashSet<>();
	
	private Map<Class<? extends Annotation>, String> annotationParamMap = new HashMap<>();
	private Map<Class<? extends Annotation>, PluginEvent> annotationEventMap = new HashMap<>();
	
	private ReadWriteLock classRWLock = new ReentrantReadWriteLock();
	private Lock readlock = classRWLock.readLock();
	private Lock writelock = classRWLock.writeLock();
		
	
	public PluginStatus getPluginStatus(String plugin) {
		readlock.lock();
		try {
			if(disabledPlugins.contains(plugin)) {
				return PluginStatus.DISABLED;
			} 
			
			if(enabledPlugins.contains(plugin)) {
				return PluginStatus.ENABLED;
			}
			
			return PluginStatus.NONE;
		} finally {
			readlock.unlock();
		}
		
	}
	public void enablePlugin(String plugin) {
		writelock.lock();
		try {
			enabledPlugins.add(plugin);
			disabledPlugins.remove(plugin);
		} finally {
			writelock.unlock();
		}
	}
	
	public void disablePlugin(String plugin) {
		writelock.lock();
		try {
			disabledPlugins.add(plugin);
			enabledPlugins.add(plugin);
		} finally {
			writelock.unlock();
		}
	}
	
	public Set<String> getEnabledPlugins() {
		readlock.lock();
		try {
			return Collections.unmodifiableSet(enabledPlugins);
		} finally {
			readlock.unlock();
		}
	}

	public Set<String> getDisabledPlugins() {
		readlock.lock();
		try {
			return Collections.unmodifiableSet(disabledPlugins);
		} finally {
			readlock.unlock();
		}
	}

	public void registerAnnotation(Class<? extends Annotation> clazz, String param) {
		writelock.lock();
		try {
			annotationParamMap.put(clazz, param);
		} finally {
			writelock.unlock();
		}
	}
	
	public void registerAnnotation(Class<? extends Annotation> clazz, PluginEvent e) {
		writelock.lock();
		try {
			annotationEventMap.put(clazz, e);
		} finally {
			writelock.unlock();
		}
	}
	
	private void addCommand(String key, MethodContainer val) {
		writelock.lock();
		try {
			if(commands.get(key) == null) {
				commands.put(key, new ArrayList<MethodContainer>());
			}
			
			commands.get(key).add(val);
		} finally {
			writelock.unlock();
		}
	}
	
	private void addMethod(PluginEvent key, MethodContainer val) {
		writelock.lock();
		try {
			if(eventMap.get(key) == null) {
				eventMap.put(key, new ArrayList<MethodContainer>());
			}
			
			eventMap.get(key).add(val);
		} finally {
			writelock.unlock();
		}
	}
	
	public List<BotResponse> triggerEvent(PluginEvent e, Map<String, String> params, PluginEventBot bot) {
		readlock.lock();
		try {
			List<MethodContainer> mcs;
			
			if(e.equals(PluginEvent.COMMAND)) {
				mcs = commands.get(params.get("command"));
			} else {
				mcs = eventMap.get(e);
			}
			
			//no events reg'd
			if(mcs == null) {
				return new ArrayList<>();
			}
			
			List<BotResponse> responses = new ArrayList<>(mcs.size());
			for(MethodContainer mc : mcs) {
				if(!disabledPlugins.contains(mc.getPlugin()) && enabledPlugins.contains(mc.getPlugin())) {
					executor.submit(new MethodContainerProcessThread(mc, params, bot));
				}
			}
			
			return responses;
		} finally {
			readlock.unlock();
		}
	}
	
	public void registerMethod(Method m, Object instance, String plugin) {
		writelock.lock();
		try {
			if(!disabledPlugins.contains(plugin)) {
				enabledPlugins.add(plugin);
			}
	
			MethodContainer mc = new MethodContainer(plugin);
			mc.setInstance(instance);
			mc.setMethod(m);
			
			Annotation[] annotations = m.getAnnotations();
			for(Annotation annotation : annotations) {
				Class<? extends Annotation > type = annotation.annotationType();
				PluginEvent e = annotationEventMap.get(type);
				
			
				if(e != null && e.equals(PluginEvent.COMMAND)) {
					OnCommand commandType = (OnCommand)annotation;
					for(String command : commandType.value()) {
						addCommand(command, mc);
					}
					
				} else if(e != null) {
					addMethod(e, mc);
				}
			}
		} finally {
			writelock.unlock();
		}
	}

	public ExecutorService getExecutor() {
		return executor;
	}

	public void setExecutor(ExecutorService executor) {
		writelock.lock();
		try {
			this.executor = executor;
		} finally {
			writelock.unlock();
		}
	}
	
	private static class MethodContainerProcessThread implements Callable<List<BotResponse>>{
		private MethodContainer mc;
		private Map<String, String> params;
		private PluginEventBot bot;

		public MethodContainerProcessThread(MethodContainer mc,
				Map<String, String> params, PluginEventBot botToRespond) {
			this.mc = mc;
			this.params = params;
			this.bot = botToRespond;
		}
		
		
		@SuppressWarnings("unchecked")
		public List<BotResponse> call() {
			List<BotResponse> responses = new ArrayList<>();
			
			try {
				Object result = mc.invoke(mc.makeArgs(params));
				if(result instanceof BotResponse) {
					responses.add((BotResponse)result);
				} else if (result instanceof Map<?,?>) {
					Map<Object,Object> resultMap = (Map<Object, Object>)result;
					
					for(Object key : resultMap.keySet()) {
						Object val = resultMap.get(key);
						
						if(val instanceof Iterable<?>) {
							for(Object o : (Iterable<Object>)val) {
								BotResponse br = new BotResponse();
								br.setTarget(key.toString());
								br.setResponse(o.toString());
								responses.add(br);
							}
						} else {
							BotResponse br = new BotResponse();
							br.setTarget(key.toString());
							br.setResponse(val.toString());
							responses.add(br);
						}
						
					}
				} else if (result instanceof Iterable) {
					Iterator<?> iter = ((Iterable<?>)result).iterator();
					while(iter.hasNext()) {
						Object o = iter.next();
						
						if(o instanceof BotResponse) {
							responses.add((BotResponse)o);
						} else {
							BotResponse br = new BotResponse();
							br.setTarget(params.get("reply"));
							br.setResponse(o.toString());
							responses.add(br);
						}
					}
				} else {
					BotResponse br = new BotResponse();
					br.setTarget(params.get("reply"));
					br.setResponse(result.toString());
					responses.add(br);
				}
			} catch (IllegalAccessException | IllegalArgumentException
					| InvocationTargetException e1) {
				LOG.error("Invocation error", e1);
			}
			
			for(BotResponse response : responses) {
				switch(response.getType()) {
					case NOTICE:
						bot.sendNotice(response.getTarget(), response.getResponse());
						break;
					case MESSAGE:
						bot.sendMessage(response.getTarget(), response.getResponse());
						break;
					case ACTION:
						bot.sendAction(response.getTarget(), response.getResponse());
						break;
				}
			}
			
			return responses;
		}
		
		
	}

	private class MethodContainer {
		private Method method;
		private Object instance;
		private String plugin;
		
		public MethodContainer(String plugin) {
			this.plugin = plugin;
		}
		public String getPlugin() {
			return plugin;
		}
		public Method getMethod() {
			return method;
		}
		public void setMethod(Method method) {
			this.method = method;
		}
		@SuppressWarnings("unused")
		public Object getInstance() {
			return instance;
		}
		public void setInstance(Object instance) {
			this.instance = instance;
		}
		
		public Object invoke(Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
			return method.invoke(instance, args);
		}
		
		public Object[] makeArgs(Map<String, String> params) {
			Method m = getMethod();
			
			Class<?>[] types = m.getParameterTypes();
			List<Object> invokeParams = new ArrayList<>(types.length);
			Annotation[][] parameterAnnotations = m.getParameterAnnotations();
			
			paramLoop:
			for (int i = 0; i < parameterAnnotations.length; i++) {
				Annotation[] paramAnns = parameterAnnotations[i];
				Class<?> type = types[i];
				
				if(!type.equals(String.class)) {
					invokeParams.add(new Object());
					continue paramLoop;
				}
				
				for(Annotation paramAnnotation : paramAnns) {
					String param = annotationParamMap.get(paramAnnotation.annotationType());
					
					if(param != null) {
						String paramVal = params.get(param);
						if(paramVal != null) {
							invokeParams.add(paramVal);
							continue paramLoop;
						}
					}
					
				}
				
				invokeParams.add("");
			}
			
			return invokeParams.toArray(new Object[invokeParams.size()]);
		}
		
	}
}
